﻿/* 
 * Copyright 2010 Justin Pealing
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

using System;
using System.IO;
using System.Collections.Generic;

namespace Minnow.Web.FastCGI
{
    /// <summary>
    /// Reads and writes stream data to and from a FastCGI process instance.
    /// </summary>
    public class FastCGIStream : Stream
    {
        BufferedConnection connection;
        private FastCGIRequest request;
        private RecordType streamType;
        private MemoryStream buffer = new MemoryStream();

        /// <summary>
        /// Initialises a new instance of <see cref="FastCGIStream" />.
        /// </summary>
        /// <param name="Connection">Connection to use to read to and write from.</param>
        /// <param name="StreamType">The type of stream.</param>
        /// <param name="Request">Request to which the stream belongs.</param>
        /// <exception cref="ArgumentNullException">Request is null.</exception>
        /// <exception cref="ArgumentNullException">Connection is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">StreamType is not a valid streaming record type.</exception>
        internal FastCGIStream(FastCGIRequest Request, BufferedConnection Connection, RecordType StreamType)
        {
            if (Request == null)
            {
                throw new ArgumentNullException("Process");
            }
            if (Connection == null)
            {
                throw new ArgumentNullException("Connection");
            }

            switch (StreamType)
            {
                case RecordType.FCGI_STDIN:
                case RecordType.FCGI_DATA:
                case RecordType.FCGI_STDOUT:
                case RecordType.FCGI_STDERR:
                    this.connection = Connection;
                    this.streamType = StreamType;
                    this.request = Request;
                    break;
                default:
                    throw new ArgumentOutOfRangeException(StreamType + " is not a valid streaming record type.");
            }
        }

        /// <summary>
        /// Gets a value indicating whether the current stream supports reading.
        /// </summary>
        public override bool CanRead
        {
            get
            {
                return this.streamType == RecordType.FCGI_STDERR || this.streamType == RecordType.FCGI_STDOUT;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the stream supports seeking. This property always returns false.
        /// </summary>
        public override bool CanSeek
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the current stream supports writing.
        /// </summary>
        public override bool CanWrite
        {
            get
            {
                return this.streamType == RecordType.FCGI_STDIN || this.streamType == RecordType.FCGI_DATA;
            }
        }

        /// <summary>
        /// Flushes data from the stream. This method is reserved for future use.
        /// </summary>
        /// <remarks>
        /// Because FastCGIStream is not buffered this has no affect. Calling the Flush method will not throw an exception.
        /// </remarks>
        public override void Flush()
        {
            
        }

        /// <summary>
        /// Gets the length of the data available on the stream. This property always throws a <see cref="NotSupportedException" />.
        /// </summary>
        public override long Length
        {
            get
            {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Gets or sets the current position in the stream. This property always throws a <see cref="NotSupportedException" />.
        /// </summary>
        public override long Position
        {
            get
            {
                throw new NotSupportedException();
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Reads data from the stream.
        /// </summary>
        /// <param name="Buffer">An array of type <see cref="Byte" /> that is the location in memory to store data read 
        /// from the stream.</param>
        /// <param name="Offset">The location in buffer to begin storing the data to.</param>
        /// <param name="Count">The number of bytes to read from the stream.</param>
        /// <returns>The number of bytes read from the stream</returns>
        /// <exception cref="ArgumentNullException">Buffer is null.</exception>
        public override int Read(byte[] Buffer, int Offset, int Count)
        {
            if (Buffer == null)
            {
                throw new ArgumentNullException("Buffer");
            }
            if (Offset < 0 || Count < 0 || Offset + Count > Buffer.Length)
            {
                throw new ArgumentOutOfRangeException();
            }

            if (this.buffer != null && this.buffer.Position < this.buffer.Length)
            {
                // If there is a buffered record, continue reading that first
                return this.buffer.Read(Buffer, Offset, Count);
            }
            else
            {
                // If the request has ended, only return buffered records
                Record? record;
                if (this.request.RequestEnded)
                {
                    record = this.connection.GetBufferedRecord(this.request.RequestId, this.streamType);
                }
                else
                {
                    record = this.connection.ReadRecord(this.request.RequestId, this.streamType);
                }

                if (record == null || record.Value.ContentLength == 0)
                {
                    return 0;
                }
                else
                {
                    if (record.Value.ContentLength < Count)
                    {
                        record.Value.ContentData.CopyTo(Buffer, Offset);
                        return record.Value.ContentData.Length;
                    }
                    else
                    {
                        this.buffer = new MemoryStream(record.Value.ContentData);
                        return this.buffer.Read(Buffer, Offset, Count);
                    }
                }
            }
        }

        /// <summary>
        /// Sets the current position of the stream to the given value. This method always throws a <see cref="NotSupportedException" />.
        /// </summary>
        /// <param name="offset">This parameter is not used.</param>
        /// <param name="origin">This parameter is not used.</param>
        /// <returns>The position in the stream. This method is not currently supported, and will throw a <see cref="NotSupportedException" />.</returns>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Sets the length of the stream. This method always throws a <see cref="NotSupportedException" />.
        /// </summary>
        /// <param name="value">This parameter is not used.</param>
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Writes data to the stream.
        /// </summary>
        /// <param name="buffer">An array of type <see cref="Byte" /> that contains the data to write to the stream.</param>
        /// <param name="offset">The location in buffer from which to start writing data.</param>
        /// <param name="count">The number of bytes to write to the stream.</param>
        public override void Write(byte[] Buffer, int Offset, int Count)
        {
            if (Buffer == null)
            {
                throw new ArgumentNullException("Buffer");
            }
            if (Offset < 0 || Count < 0 || Offset + Count > Buffer.Length)
            {
                throw new ArgumentOutOfRangeException();
            }
            this.connection.Connection.SendRecord(this.request.RequestId, this.streamType, Buffer, Offset, Count);
        }

        /// <summary>
        /// Closes the stream, sending the end of stream indicator to the FastCGI process.
        /// </summary>
        public override void Close()
        {
            this.connection.Connection.SendRecord(this.request.RequestId, this.streamType);
            base.Close();
        }
    }
}
